/* dom操作 */
const { JSDOM } = require('jsdom');
const fetch = require('node-fetch');
const Fs = require('@lib/utils/fs');
const {
  isValidURL
} = require('@lib/utils/tools');
const CONSTS = require('@lib/consts/index');

class Dom {
  /**
   * 校验img标签是否设置了width或height
   * @param {*} imgElement img标签元素
   * @param {*} doc document对象
   * @param {*} win window对象
   * @param {*} cssList css文件列表，每一项包括：文件名fileName、文件路径filePath、扩展名ext、文件大小size(KB)
   * @returns 是否设置了width或height-布尔值
   */
  static async validateImgWorH(imgElement, doc, win, cssList) {
    // 1.检查img标签上是否设置了width或height
    if (imgElement.hasAttribute('width') || imgElement.hasAttribute('height')) {
      return true;
    }
    // 2.检查标签上的style属性中是否设置了width或height
    if (imgElement.style.width || imgElement.style.height) {
      return true;
    }
    // 3.检查内嵌文件style是否包含设置width或height
    const computedStyle = win.getComputedStyle(imgElement);
    if (
      (computedStyle.getPropertyValue('width') &&
        computedStyle.getPropertyValue('width') !== 'auto') ||
      (computedStyle.getPropertyValue('height') &&
        computedStyle.getPropertyValue('height') !== 'auto')
    ) {
      return true;
    }
    // 4.检查外部样式表CSS文件是否包含设置width或height
    // 4.1 获取document对象中的外部样式表CSS文件
    const stylesheets = Dom.getExternalStyleSheets(doc);
    for (const item of stylesheets) {
      // 4.2 根据外部样式表的href获取最终需要解析的linkUrl
      let linkUrl = item.href;
      let isPrjFile = 0; // 是否是项目内文件，0-否，1-是
      if (!isValidURL(item.href)) {
        const fileName = Fs.getFileNameByRelativePath(item.href);
        const linkTarget = cssList.find(item => item.filePath.indexOf(fileName) > -1);
        if (linkTarget) {
          linkUrl = linkTarget.filePath;
          isPrjFile = 1;
        }
      }
      // 4.3 加载并解析外部样式表，获取其css规则
      const rules = await Dom.parseStyleSheet(linkUrl, isPrjFile);
      // 4.4 遍历规则，是否与标签节点匹配
      for (let j = 0; j < rules.length; j++) {
        const rule = rules[j];
        const cssRule = rule.selectorText;
        // 检查DOM节点是否与CSS规则匹配
        const isMatched = imgElement.matches(cssRule);
        if (isMatched && (rule.style.getPropertyValue('width') || rule.style.getPropertyValue('height'))) {
          return true;
        }
      }
    }
    return false;
  };

  /**
   * 传入document对象获取外部样式表
   * @param {*} doc document对象
   * @returns 外部样式表，每一项包括href和media
   */
  static getExternalStyleSheets(doc) {
    const linkElements = doc.querySelectorAll('link[rel="stylesheet"]');
    const styleSheets = Array.from(linkElements).map((linkElement) => {
      return {
        href: linkElement.href,
        media: linkElement.media
      };
    });
    return styleSheets;
  };

  /**
   * 加载并解析外部样式表，获取其css规则
   * @param {*} url 外部样式表url
   * @param {*} isPrjFile 是否是项目内文件，0-否，1-是
   * @returns 样式规则数组
   */
  static async parseStyleSheet(url, isPrjFile) {
    try {
      let cssText = '';
      if (+isPrjFile === 0) {
        const response = await fetch(url);
        cssText = await response.text();
      } else if (+isPrjFile === 1) {
        cssText = Fs.readFileSync(url);
      }
      const dom = new JSDOM('');
      const doc = dom.window.document;
      const styleSheet = doc.createElement('style');
      styleSheet.textContent = cssText;
      doc.head.appendChild(styleSheet);
      const rules = Array.from(styleSheet.sheet.cssRules || styleSheet.sheet.rules);
      return rules;
    } catch (error) {
      console.error('Error parsing stylesheeet:', error);
      return [];
    }
  };

  /**
   * 检查过深的 DOM 嵌套
   * @param {*} element dom元素节点
   * @param {*} depth 当前嵌套深度
   * @param {*} depthLimit 嵌套深度限制
   * @returns isExceed-是否超过嵌套深度限制，element-超过限制的DOM节点
   */
  static checkNestedDepth(element, depth = 1, depthLimit) {
    const children = element.children;
    if (depth > depthLimit) {
      return {
        isExceed: true,
        element
      };
    }
    for (let i = 0; i < children.length; i++) {
      const { isExceed, element } = Dom.checkNestedDepth(children[i], depth + 1, depthLimit);
      if (isExceed) {
        return { isExceed, element };
      }
    }
    return {
      isExceed: false,
      element: null
    };
  }

  /**
   * 检查错误的DOM节点或使用了冗余的标签
   * @param {*} element dom元素节点
   * @param {*} errList 所有存在错误节点存放的数组
   */
  static traverseCheckStructure(element, errList) {
    const children = element.children;
    // 冗余标签检查原则：该标签可以直接存放文本 且 子节点只有1个，这个子节点只包含文本
    // 如下所示：
    // <div><span>This is a redundant span tag</span></div>
    // element.tagName=DIV，element.children[0].tagName=SPAN
    if (
      CONSTS.STORE_TEXT_TAGS.includes(element.tagName.toLowerCase()) &&
      children.length === 1 &&
      children[0].nodeType === 1 &&
      children[0].textContent.trim() &&
      !children[0].children.length
    ) {
      errList.push({
        warnMsg: `【!尽量避免】可能存在冗余的标签${children[0].tagName.toLowerCase()}`
      });
      return;
    }
    // 空或者不可访问的标签，如下所示：
    // <div id="app"></div>
    if (
      !CONSTS.SELF_CLOSE_TAGS.includes(element.tagName.toLowerCase()) &&
      !children.length &&
      !element.textContent.trim() &&
      !element.hasAttribute('aria-label')
    ) {
      errList.push({
        warnMsg: `【!尽量避免】可能存在空或者不可访问的标签${element.tagName.toLowerCase()}`
      });
      return;
    }
    for (let i = 0; i < children.length; i++) {
      Dom.traverseCheckStructure(children[i], errList);
    }
  }
}

module.exports = Dom;
